(function () {
  const blankChar = { ' ': 1, '\t': 1, '\n': 1, '\r': 1 }
  function nextToken(input) {
    const ch = input.next()
    switch (ch) {
      case '%':
        while (1) {
          const ch2 = input.next()
          if (ch2 == '\n') {
            return nextToken(input)
          }
          if (ch2 == null) {
            return 'EOF'
          }
        }
      case ':':
        let name = ':'
        while (1) {
          const ch2 = input.next()
          if (!/[A-Za-z0-9]/.test(ch2)) {
            input.prev()
            return name
          }
          name += ch2
        }
    }
    if (blankChar[ch]) {
      while (1) {
        if (!blankChar[input.next()]) {
          input.prev()
          return nextToken(input)
        }
      }
    }
    if (ch == '0') { return 0 }
    if (ch > '0' && ch <= '9') {
      let num = ch
      while (1) {
        const ch2 = input.next()
        if (ch2 >= '0' && ch2 <= '9') {
          num += ch2
        } else {
          input.prev()
          return Number(num)
        }
      }
    }
    return ch || 'EOF'
  }

  const letterPitches = { A: 0, B: 2, C: 3, D: 5, E: 7, F: 8, G: 10 }
  const numberPitches = { 1: 0, 2: 2, 3: 4, 4: 5, 5: 7, 6: 9, 7: 11 }
  function error(token, should) {
    throw `Unexpected token '${token}'` + (should == null ? '' : `, want '${should}'`)
  }
  function check(should, token) {
    return should == token
  }
  function checkNumber(token) {
    return typeof(token) == 'number'
  }
  function checkChord(token) {
    return typeof(token) == 'string' && token.startsWith(':')
  }
  function addNote(track, items, duration) {
    const last = track[track.length - 1]
    track.push({
      type: 'note',
      time: last ? last.time + last.duration : 0,
      items,
      duration
    })
  }

  function parse(input) {
    const tracks = []
    tracks[-1] = []
    tracks[-1]._key = 0
    tracks[-1]._time = 64
    for (const attr of ['key', 'time']) {
      Object.defineProperty(tracks[-1], attr, {
        get() { return this['_' + attr] },
        set(v) { this['_' + attr] = v; tracks.forEach(t => t[attr] = v) }
      })
    }
    let track = tracks[-1]
    let trackIndex = -1
    const chords = {}
    function switchTrack(index) {
      trackIndex = index
      if (!tracks[index]) {
        tracks[index] = []
        tracks[index].key = tracks[-1].key
        tracks[index].time = tracks[-1].time
      }
      track = tracks[index]
    }
    function top() {
      return track[track.length - 1]
    }
    function changeNote(delta) {
      top().item += delta
    }
    function _(token) {
      if (!check(token, input.current)) {
        error(input.current, token)
      }
      input.next()
    }
    function DOC() {
      switch (input.current) {
        case '[':
          input.next()
          CMDS()
          _(']')
          DOC()
          break
        case '{':
          input.next()
          switchTrack(0)
          TRACKS()
          _('}')
          switchTrack(-1)
          DOC()
          break
        case 'EOF':
          break
        default:
          error(input.current)
      }
    }
    function TRACKS() {
      _('|')
      BARS()
      if (check('|', input.current)) {
        switchTrack(trackIndex + 1)
        TRACKS()
      }
    }
    function BARS() {
      const beginIndex = track.length
      track.barBeginIndex = beginIndex
      track.barExtraDuration = 0
      NOTES()
      _('|')
      let time = track.barExtraDuration
      for (let i = beginIndex; i < track.length; i++) {
        time += track[i].duration
      }
      if (time != track.time) {
        throw `Bar duration should be ${track.time / 16} quarter note(s), got ${time / 16}`
      }
      if (NOTES.isHead(input.current)) {
        BARS()
      }
    }
    function CMDS() {
      CMD()
      if (check(',', input.current)) {
        input.next()
        CMDS()
      }
    }
    function CMD() {
      if (input.current == 1) {
        input.next()
        _('=')
        if (!(input.current >= 'A' && input.current <= 'G')) {
          throw '(1=) should be followed by A~G'
        }
        track.push({ type: 'item', item: letterPitches[input.current] })
        if (SUFFIX.head[input.next()]) {
          SUFFIX()
        }
        track.key = track.pop().item
      } else if (checkNumber(input.current) && input.current >= 1 && input.current <= 99) {
        const a = input.current
        input.next()
        _('/')
        const b = input.current
        if ([2, 4, 8, 16, 32, 64].indexOf(b) < 0) {
          error(b)
        }
        track.time = 64 / b * a
        input.next()
      } else if (SUFFIX.head[input.current]) {
        track.push({ type: 'item', item: track.key })
        SUFFIX()
        track.key = track.pop().item
      } else if (checkChord(input.current)) {
        const chordName = input.current
        input.next()
        _('=')
        const realKey = track.key
        track.key = 0
        ITEMS()
        track.key = realKey
        chords[chordName] = track.pop().items
      } else {
        error(input.current)
      }
    }
    function NOTES() {
      NOTE()
      if (NOTES.isHead(input.current)) {
        NOTES()
      }
    }
    NOTES.shead = { '[': 1, '-': 1, '\'': 1, '.': 1 }
    NOTES.isHead = e => checkNumber(e) || NOTES.shead[e]
    function NOTE() {
      if (checkNumber(input.current)) {
        if (input.current == 0) {
          addNote(track, [], 16)
          input.next()
        } else {
          ITEMS()
        }
      } else if (input.current == '[') {
        input.next()
        CMDS()
        _(']')
      } else if (input.current == '\'') {
        const note = top()
        if (!note || note.type != 'note') { throw '(\') should appear after a note' }
        const dur = note.duration / 2
        if (dur != Math.floor(dur)) { throw 'Mininum note duration exceeded' }
        if (track.length - 1 < track.barBeginIndex) { throw '(\') should not appear at the beginning of a bar' }
        note.duration = dur
        input.next()
      } else if (input.current == '-') {
        const note = top()
        if (!note || note.type != 'note') { throw '(-) should appear after a note' }
        const exdur = 16
        if (track.length - 1 < track.barBeginIndex) {
          track.barExtraDuration += exdur
        }
        note.duration += exdur
        input.next()
      } else if (input.current == '.') {
        const note = top()
        if (!note || note.type != 'note') { throw '(.) should appear after a note' }
        const exdur = note.duration / 2
        if (exdur != Math.floor(exdur)) { throw 'Mininum note duration exceeded' }
        if (track.length - 1 < track.barBeginIndex) {
          track.barExtraDuration += exdur
        }
        note.duration += exdur
        input.next()
      } else {
        error(input.current)
      }
    }
    function ITEMS() {
      ITEM()
      if (ADDS.head[input.current]) {
        ADDS()
      }
    }
    function ADDS() {
      ADD()
      if (ADDS.head[input.current]) {
        ADDS()
      }
    }
    ADDS.head = { '&': 1, '\\': 1 }
    function ADD() {
      switch (input.current) {
        case '&':
          input.next()
          SINGLE()
          {
            const item = track.pop()
            const note = top()
            if (note.items.indexOf(item.item) >= 0) {
              throw 'Note already exists in the chord'
            }
            note.items.push(item.item)
          }
          break
        case '\\':
          input.next()
          SINGLE()
          {
            const item = track.pop()
            const note = top()
            const ind = note.items.indexOf(item.item)
            if (ind < 0) {
              throw 'Note does not exist in the chord'
            }
            note.items.splice(ind, 1)
          }
          break
        default:
          error(input.current)
      }
    }
    function ITEM() {
      SINGLE()
      const base = track.pop().item
      if (checkChord(input.current)) {
        const chord = chords[input.current]
        if (!chord) {
          throw 'Unknown chord ' + input.current
        }
        addNote(track, chord.map(i => base + i), 16)
        input.next()
      } else {
        addNote(track, [base], 16)
      }
    }
    function SINGLE() {
      if (!checkNumber(input.current) || !(input.current >= 1 && input.current <= 7)) {
        error(input.current)
      }
      track.push({ type: 'item', item: track.key + numberPitches[input.current] })
      if (SUFFIX.head[input.next()]) {
        SUFFIX()
      }
    }
    function SUFFIX() {
      switch (input.current) {
        case '#':
          changeNote(1)
          break
        case 'x':
          changeNote(2)
          break
        case 'b':
          changeNote(-1)
          break
        case '^':
          changeNote(12)
          break
        case '_':
          changeNote(-12)
          break
        default:
          error(input.current)
      }
      if (SUFFIX.head[input.next()]) {
        SUFFIX()
      }
    }
    SUFFIX.head = { '#': 1, 'x': 1, 'b': 1, '^': 1, '_': 1 }
    DOC()
    if (tracks.length <= 0) {
      throw 'No tracks found'
    }
    const result = {
      tracks: [],
      notes: []
    }
    for (const _ in tracks) {
      result.tracks.push({
        volume: 0.1 / tracks.length,
        noteFormat: 'customWave',
        noteType: [
          [0, 1, 0.4, 0.6, 0.2],
          [0, 0, 0, 0, 0]
        ],
        basePitch: 0
      })
    }
    const tracksLeft = tracks.slice()
    let count = 0
    for (let i = 0; i < tracks.length; i++) {
      const track = tracks[i]
      track._index = i
      track._noteIndex = 0
      count += track.length
    }
    while (count > 0) {
      let minTime = Infinity, nextTrack
      for (const track of tracksLeft) {
        const time = track[track._noteIndex].time
        if (time < minTime) {
          minTime = time
          nextTrack = track
        }
      }
      const nextNote = nextTrack[nextTrack._noteIndex]
      for (const item of nextNote.items) {
        result.notes.push([nextTrack._index, nextNote.time, item, nextNote.duration])
      }
      if (++nextTrack._noteIndex >= nextTrack.length) {
        tracksLeft.splice(tracksLeft.indexOf(nextTrack), 1)
      }
      count--
    }
    if (result.notes.length <= 0) {
      throw 'No notes found'
    }
    result.notes.forEach(n => {
      n[1] *= 15
      n[3] *= 15
    })
    return result
  }

  function compile(source) {
    const reader = {
      index: -1,
      next() {
        return source[++reader.index]
      },
      prev() {
        return source[--reader.index]
      }
    }
    const lexer = { next: () => lexer.current = nextToken(reader) }
    lexer.next()
    return parse(lexer)
  }

  if (typeof window == 'undefined') {
    if (!process.argv[2]) {
      console.error(`Usage: ${process.argv[0]} ${process.argv[1]} <source file>`)
      return
    }
    const sourceName = process.argv[2]
    const fs = require('fs')
    const source = fs.readFileSync(sourceName, { encoding: 'utf-8' })
    const result = JSON.stringify(compile(source))
    fs.writeFileSync(sourceName + '.json', result, { encoding: 'utf-8' })
  } else {
    window.nmlc = compile
  }
})()

/*

DOC     : EOF
        | '[' CMDS(number,'#','x','b','^','_') ']' DOC('[','{',EOF)
        | '{' TRACKS('|') '}' DOC('[','{',EOF)

TRACKS  : '|' BARS(number,'[','-','\'','.')
        | '|' BARS(number,'[','-','\'','.') TRACKS('|')

BARS    : NOTES(number,'[','-','\'','.') '|'
        | NOTES(number,'[','-','\'','.') '|' BARS(number,'[','-','\'','.')

CMDS    : CMD(number,'#','x','b','^','_')
        | CMD(number,'#','x','b','^','_') ',' CMDS(number,'#','x','b','^','_')

CMD     : number(1) '=' ('A','B','C','D','E','F','G')
        | number(1) '=' ('A','B','C','D','E','F','G') SUFFIX('#','x','b','^','_')
        | number(1~99) '/' number(2,4,8,16,32,64)
        | SUFFIX('#','x','b','^','_')
        | chord '=' ITEMS(number)

NOTES   : NOTE(number,'[','-','\'','.')
        | NOTE(number,'[','-','\'','.') NOTES(number,'[','-','\'','.')

NOTE    : number(0)
        | ITEMS(number)
        | '[' CMDS(number,'#','x','b','^','_') ']'
        | '\''
        | '-'
        | '.'

ITEMS   : ITEM(number)
        | ITEM(number) ADDS('&','\\')

ADDS    : ADD('&','\\')
        | ADD('&','\\') ADDS('&','\\')

ADD     : '&' SINGLE(number)
        | '\\' SINGLE(number)

ITEM    : SINGLE(number)
        | SINGLE(number) chord

SINGLE  : number(1~7)
        | number(1~7) SUFFIX('#','x','b','^','_')

SUFFIX  : ('#','x','b','^','_')
        | ('#','x','b','^','_') SUFFIX('#','x','b','^','_')

*/
